Part 8 - Plans入門

コンテキスト

Federated Learningを大規模に実運用しようと思った際に重要なオブジェクトとなる、Planという概念について紹介します。Planは使用するネットワーク帯域を劇的に減らし、非同期処理を実現し、リモートデイバイスに自律性を与えてくれます。元となるアイデアはこの論文、Towards Federated Learning at Scale: System Design、を参照してください。現在はPySyftライブラリのニーズに応じて、一部変更が加えられています。

Planは、関数のように、連続するオペレーションを纏める目的で作られています。しかし、Planを使えば、定義した一連のオペレーションを一回のメッセージでリモートのワーカーに送ることができます。こうすることで、N個の(オペレーションの)メッセージを送る代わりに1つのメッセージを送るだけで、ポインタを通してN個のオペレーションを参照できます。PlanにはTensor("state tensors"と呼ばれます)をつけて送ることもできます。"state tensors"は引数のようなものです。Planは送信可能な関数と捉えることもできますし、リモートにて実行可能なクラスと捉えることもできます。これによって、高次のユーザーはPlanの概念を特に意識することなく、恣意的な連続するPyTorchの関数をリモートワーカーに送ることが可能になります。

一点注意が必要な点は、現時点ではPlanで使用可能な関数はPyTorchのHook機能を持つオペレーションに限定されています。これは if, for そしrて while といった論理構造が使えないことを意味します。私たちはこの件について対応中です。

正確には、これらのオペレーションを使うことはできますが、最初のコンピューテーションで取った分岐がその後の全てのコンピューテーションに適応されてしまいます。これでは都合が悪いですよね。

Authors:

インポートとモデル定義

まずは、通常のPyTorchのインポート処理を行いましょう


In [ ]:
import torch
import torch.nn as nn
import torch.nn.functional as F

次にPySyft用のコードです。一つ覚えておくべきことは、ローカルワーカーはクライアントワーカーになるべきではないという事です。 クライアンワーカー以外はPlanの実行に必要なオブジェクトを保持できません。ここで言うローカルワーカーとは私たちで、クライアントワーカーとはリモートワーカーの事です。


In [ ]:
import syft as sy  # Pysyftライブラリをインポート
hook = sy.TorchHook(torch)  # PyTorchをホック ie torchを拡張します

# IMPORTANT: ローカルワーカーはクライアントワーカーになることは出来ません
hook.local_worker.is_client_worker = False

server = hook.local_worker

説明の通り、リモートワーカー(デバイス)を定義します。 そして、彼らにデータを割り当てます。


In [ ]:
x11 = torch.tensor([-1, 2.]).tag('input_data')
x12 = torch.tensor([1, -2.]).tag('input_data2')
x21 = torch.tensor([-1, 2.]).tag('input_data')
x22 = torch.tensor([1, -2.]).tag('input_data2')

device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) 
device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22))
devices = device_1, device_2

基本的な例

では、Planとして纏めたい関数を定義しましょう。そのために行うことは、ほとんど関数の上にデコレータを記述するだけです。


In [ ]:
@sy.func2plan()
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

Planが作成できました。チェックしてみましょう。


In [ ]:
plan_double_abs

Planの使用には2つのステップがあります。まず、ビルド(これは関数の中の連続するオペレーションを登録するような事です)です。次にビルドしたPlanをワーカー(デバイス)へ送ります。簡単な作業で実現できます。

Planのビルド

Planをビルドするには、何かしらデータを付けてコールしてあげるだけでOKです。 まずは、リモートデータの参照を取得しましょう。参照取得のリクエストはネットワーク越しに送信され、ポインタが返されます。


In [ ]:
pointer_to_data = device_1.search('input_data')[0]
pointer_to_data

もし、ここでlocation:device_1のデバイス上でPlayを実行しようとすると、エラーになってしまいます。まだビルドが出来ていないからです。


In [ ]:
plan_double_abs.is_built

In [ ]:
# ビルドされていないPlanをリモートワーカーへ送ろうとするとエラーになります
try:
    plan_double_abs.send(device_1)
except RuntimeError as error:
    print(error)

Planをビルドするには、必要な引数(何らかのデータ)を渡しつつbuildコマンドを実行してください。Planがビルドされると全てのコマンドはローカルワーカーによって、順番に実行され、結果はPlayのactions属性にキャッシュされます。


In [ ]:
plan_double_abs.build(torch.tensor([1., -2.]))

In [ ]:
plan_double_abs.is_built

このPlayを再度送ってみましょう。今度はうまく行きます。


In [ ]:
# 今回はエラーは出ません
pointer_plan = plan_double_abs.send(device_1)
pointer_plan

Tensorの時と同様にポインタが取得できます。PointerPlanという名前は分かり易いですね。

特筆すべきことの一つは、Planはビルドされる時にコンピューテーションの結果として割り当てられるIDが事前に設定されます。これにより、リモートマシンのコンピューテーションの結果を待たずしてIDを取得でき、コマンドは非同期で送信が可能になります。例えば、device_1でのバッチ処理の結果を待たずにdevice_2で次のバッチ処理を実行することが可能になります。

Planをリモートで実行する

ポインタにデータを引数として渡す事により、Planをリモート環境で実行することができます。Plan実行結果は事前に定義された場所(結果格納場所はコンピューテーションの前に事前に設定されます)に格納されます。 結果はシンプルにポインタです。他のPyTorchのオペレーションをリモートで実行した時と同じです。


In [ ]:
pointer_to_result = pointer_plan(pointer_to_data)
print(pointer_to_result)

そして、計算された値は、今までと同様の手法で受け取ることが可能です。


In [ ]:
pointer_to_result.get()

もう少し実践的な例

ところで、私たちがやりたいのは、PlanをFederated Learningを使ったディープラーニングに応用することですよね。では、ニューラルネットを扱かったもう少しだけ複雑な例をみてみましょう。

注記: 私たちは通常のクラスをPlanへ変更しています。これはnn.Moduleのかわりにsy.Planを継承することで実現できます。


In [ ]:
class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

In [ ]:
net = Net()

In [ ]:
net

ダミーデータを使ってビルドしてみましょう。


In [ ]:
net.build(torch.tensor([1., 2.]))

次に、Planをリモートワーカーへ送ってみましょう


In [ ]:
pointer_to_net = net.send(device_1)
pointer_to_net

リモートデータのポインタを取得しましょう


In [ ]:
pointer_to_data = device_1.search('input_data')[0]

構文的にはリモートマシン上で逐次的にオペレーションを実行するのと何らかわりはありません。ですが、この手法では複数のオペレーションが一回のコミュニケーションで実行されています。


In [ ]:
pointer_to_result = pointer_to_net(pointer_to_data)
pointer_to_result

データの受け取りはいつも通りです。


In [ ]:
pointer_to_result.get()

ジャジャーン!ローカルワーカー(サーバー、この場合は私たち?)とリモートデバイスの間のコミュニケーションを劇的に減らすことに成功しました!

ワーカー間での使い回し

私たちが欲しい重要な機能の一つは、一つのPlanを複数のワーカー間で使い回すことです。 得に、新しいワーカーのためにイチイチPlanをビルドするのは避けたいですよね。 どうすれば良いか、先ほどのニューラルネットを例に、やってみましょう。


In [ ]:
class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

In [ ]:
net = Net()

# Planのビルド
net.build(torch.tensor([1., 2.]))

メインのステップです。


In [ ]:
pointer_to_net_1 = net.send(device_1)
pointer_to_data = device_1.search('input_data')[0]
pointer_to_result = pointer_to_net_1(pointer_to_data)
pointer_to_result.get()

実は、同じPlanから別のPointerPlansをビルドすることが可能です。別のデバイスでPlanをリモート実行する時と同じです。


In [ ]:
pointer_to_net_2 = net.send(device_2)
pointer_to_data = device_2.search('input_data')[0]
pointer_to_result = pointer_to_net_2(pointer_to_data)
pointer_to_result.get()

注記: この例では、Planは一つのオペレーションしか実行しています。実行されていたのはforwardです。

Planの自動ビルド

@ sy.func2planをつけることで、Planを自動的にビルドすることが可能です。この場合、Planは定義と同時にビルドされるので、明示的にbuildする必要はありません。

この機能を有効にするために必要なことは、args_shapeという名前の引数を渡すことだけです;


In [ ]:
@sy.func2plan(args_shape=[(-1, 1)])
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

plan_double_abs.is_built

args_shapeはPlanをビルドする際にダミーデータを作成するのに使われます。


In [ ]:
@sy.func2plan(args_shape=[(1, 2), (-1, 2)])
def plan_sum_abs(x, y):
    s = x + y
    return torch.abs(s)

plan_sum_abs.is_built

state引数を使って実データを渡すことも可能です。


In [ ]:
@sy.func2plan(args_shape=[(1,)], state=(torch.tensor([1]), ))
def plan_abs(x, state):
    bias, = state.read()
    x = x.abs()
    return x + bias

In [ ]:
pointer_plan = plan_abs.send(device_1)
x_ptr = torch.tensor([-1, 0]).send(device_1)
p = pointer_plan(x_ptr)
p.get()

もっと知りたい方はチュートリアルPart 8 bisを参照してください。

PySyftのGitHubレポジトリにスターをつける

一番簡単に貢献できる方法はこのGitHubのレポジトリにスターを付けていただくことです。スターが増えると露出が増え、より多くのデベロッパーにこのクールな技術の事を知って貰えます。

Slackに入る

最新の開発状況のトラッキングする一番良い方法はSlackに入ることです。 下記フォームから入る事ができます。 http://slack.openmined.org

コードプロジェクトに参加する

コミュニティに貢献する一番良い方法はソースコードのコントリビューターになることです。PySyftのGitHubへアクセスしてIssueのページを開き、"Projects"で検索してみてください。参加し得るプロジェクトの状況を把握することができます。また、"good first issue"とマークされているIssueを探す事でミニプロジェクトを探すこともできます。

寄付

もし、ソースコードで貢献できるほどの時間は取れないけど、是非何かサポートしたいという場合は、寄付をしていただくことも可能です。寄附金の全ては、ハッカソンやミートアップの開催といった、コミュニティ運営経費として利用されます。

OpenMined's Open Collective Page


In [ ]: